/**
 * echarts 值轴分段刻度计算方法
 *
 * @desc echarts基于Canvas，纯Javascript图表库，提供直观，生动，可交互，可个性化定制的数据统计图表。
 * @author Kener (@Kener-林峰, kener.linfeng@gmail.com)
 * @author xieshiwei (谢世威, i6ma@i6ma.com)
 *
 */
/**
 * 最值、跨度、步长取近似值
 * 注意：不适用于高精度需求，或者很多位有效数字的情况！！！
 * @function    smartSteps
 * @param       {Number}    min             最小值
 * @param       {Number}    max             最大值
 * @param       {Number}    [section]       段数只能是 [0, 99] 的整数，段数为 0 或者不指定段数时，将自动调整段数
 * @param       {Object}    [opts]          其它扩展参数
 * @param       {Array}     opts.steps      自定义步长备选值，如 [10, 12, 15, 20, 25, 30, 40, 50, 60, 80] ，但必须 => [10, 99]
 * @return      {Object}    {min: 新最小值, max: 新最大值, secs: 分段数, step: 每段长, fix: 小数保留位数, pnts: [分段结果]}
 */


var mySteps = [
    10,
    20,
    25,
    50
];
var mySections = [
    4,
    5,
    6
];
var custOpts;
var custSteps;
var custSecs;
var minLocked;
var maxLocked;
var MT = Math;
var MATH_ROUND = MT.round;
var MATH_FLOOR = MT.floor;
var MATH_CEIL = MT.ceil;
var MATH_ABS = MT.abs;
function MATH_LOG(n) {
    return MT.log(MATH_ABS(n)) / MT.LN10;
}
function MATH_POW(n) {
    return MT.pow(10, n);
}
function MATH_ISINT(n) {
    return n === MATH_FLOOR(n);
}
function smartSteps(min, max, section, opts) {
    // 拿公共变量来接收 opts.steps 这个参数，就不用带着参数层层传递了，注意在函数的最终出口处释放这个值
    custOpts = opts || {};
    custSteps = custOpts.steps || mySteps;
    custSecs = custOpts.secs || mySections;
    section = MATH_ROUND(+section || 0) % 99;
    // 段数限定在 [0, 99] ，0 则自适应
    min = +min || 0;
    max = +max || 0;
    minLocked = maxLocked = 0;
    if ('min' in custOpts) {
        min = +custOpts.min || 0;
        minLocked = 1;
    }
    if ('max' in custOpts) {
        max = +custOpts.max || 0;
        maxLocked = 1;
    }
    if (min > max) {
        max = [
            min,
            min = max
        ][0];
    }
    // 最值交换
    var span = max - min;
    if (minLocked && maxLocked) {
        return bothLocked(min, max, section);    // 两个最值同时被锁定，注意差值为 0 的情况
    }
    if (span < (section || 5)) {
        // 跨度值小于要分的段数，步长将会小于 1
        if (MATH_ISINT(min) && MATH_ISINT(max)) {
            // 步长小于 1 同时两个最值都是整数，特别处理
            return forInteger(min, max, section);    // 也要考虑差值为 0 的情况
        } else if (span === 0) {
            // 非整数且跨度为 0 的情况
            return forSpan0(min, max, section);
        }
    }
    return coreCalc(min, max, section);    // 非特殊情况的计算，须确保 min < max
}
/**
 * 构造返回值，处理小数精度等问题
 * @param   {Number}    newMin      最小值
 * @param   {Number}    newMax      最大值
 * @param   {Number}    section     分段数
 * @param   {Number}    [expon]     计算量级
 * @return  {Object}                同 smartSteps
 */
function makeResult(newMin, newMax, section, expon) {
    expon = expon || 0;
    // 这是中间计算量级，受步长增长、特别是最值锁定的影响，可能会小于基准量级，因为整数部分被过度放大
    var expStep = expNum((newMax - newMin) / section, -1);
    var expMin = expNum(newMin, -1, 1);
    // 锁定的最值有效数位可能很多，需要全精度保留
    var expMax = expNum(newMax, -1);
    var minExp = MT.min(expStep.e, expMin.e, expMax.e);
    // 这个值实际上就是各值整数部分尾部多余的 0 的个数
    if (expMin.c === 0) {
        // 0 可以有任意多个尾0
        minExp = MT.min(expStep.e, expMax.e);
    } else if (expMax.c === 0) {
        minExp = MT.min(expStep.e, expMin.e);
    }
    expFixTo(expStep, {
        c: 0,
        e: minExp
    });
    expFixTo(expMin, expStep, 1);
    expFixTo(expMax, expStep);
    expon += minExp;
    // 最终的基准量级，在这个量级下，各值刚好能表示成整数
    newMin = expMin.c;
    newMax = expMax.c;
    var step = (newMax - newMin) / section;
    var zoom = MATH_POW(expon);
    var fixTo = 0;
    var points = [];
    for (var i = section + 1; i--;) {
        // 因为点数比段数多 1
        points[i] = (newMin + step * i) * zoom;    // 如果不涉及小数问题，这里就直接使用数值型
    }
    if (expon < 0) {
        fixTo = decimals(zoom);
        // 前面已经去掉了各值尾部多余的 0 ，所以 zoom 的小数位就是最终的 fix 位数
        step = +(step * zoom).toFixed(fixTo);
        // toFixed 处理长尾小数问题，如：0.2 * 0.1 = 0.020000000000000004
        newMin = +(newMin * zoom).toFixed(fixTo);
        newMax = +(newMax * zoom).toFixed(fixTo);
        for (var i = points.length; i--;) {
            points[i] = points[i].toFixed(fixTo);
            // 为保证小数点对齐，统一转为字符型
            +points[i] === 0 && (points[i] = '0');    // 0.000 不好看
        }
    } else {
        newMin *= zoom;
        newMax *= zoom;
        step *= zoom;
    }
    custSecs = 0;
    custSteps = 0;
    custOpts = 0;
    // 这些公共变量可能持用了对用户参数的引用，这里是函数的最终出口，释放引用
    return {
        min: newMin,
        // 新最小值
        max: newMax,
        // 新最大值
        secs: section,
        // 分段数
        step: step,
        // 每段长
        fix: fixTo,
        // 小数保留位数，0 则为整数
        exp: expon,
        // 基准量级，并非原值所在的量级，而是说在这个量级下，各值能表示成整数
        pnts: points    // 分段结果，整数都是数值型，小数时为了对齐小数点，都是字符型，但其中 0 不带小数点，即没有 "0.000"
    };
}
/**
 * 量级计数法 表示数值，不适用于很大或者很小的数，0 更不行
 * @param       {Number}    num             原数
 * @param       {Number}    [digit = 2]     精度位数，必须 => [1, 9]
 * @param       {Boolean}   [byFloor = 0]   默认为 0 表示近似值不小于原值，置 1 表示近似值不大于原值
 * @return      {Object}    {c: c, e: e}    c e 都是整数，c * 10 ^ e 即为原值的近似数
 * @description             返回值应该更详细一点：{c: c, e: e, d: d, n: n} ，其中 d 是 c 的位数，n = c * 10 ^ e ，不过目前好像不太有用
 */
function expNum(num, digit, byFloor) {
    digit = MATH_ROUND(digit % 10) || 2;
    if (digit < 0) {
        // 全精度位数
        if (MATH_ISINT(num)) {
            // 整数的全精度位数，要去掉尾 0 ，但 0 也是整数，要专门留一位精度
            digit = ('' + MATH_ABS(num)).replace(/0+$/, '').length || 1;
        } else {
            // 小数的全精度位数，要去掉首 0
            num = num.toFixed(15).replace(/0+$/, '');
            // toFixed 处理长尾小数
            digit = num.replace('.', '').replace(/^[-0]+/, '').length;
            num = +num;    // '' + 0.0000001 会得到 '1e-7'
        }
    }
    var expon = MATH_FLOOR(MATH_LOG(num)) - digit + 1;
    var cNum = +(num * MATH_POW(-expon)).toFixed(15) || 0;
    // toFixed 处理长尾小数问题
    cNum = byFloor ? MATH_FLOOR(cNum) : MATH_CEIL(cNum);
    // 向上取整可能发生进位，使精度位数增加 1
    !cNum && (expon = 0);
    if (('' + MATH_ABS(cNum)).length > digit) {
        // 整数位数判断，字符串法比对数法快近一倍
        expon += 1;
        cNum /= 10;
    }
    return {
        c: cNum,
        e: expon
    };
}
/**
 * 将前者的指数对齐到后者，如果前者量级较小，就是强制加大指数，值误差可能严重放大，甚至值变为 0
 */
function expFixTo(expnum1, expnum2, byFloor) {
    var deltaExp = expnum2.e - expnum1.e;
    if (deltaExp) {
        expnum1.e += deltaExp;
        // 指数减小时，只需将整数部分相应放大
        expnum1.c *= MATH_POW(-deltaExp);
        // 指数增加时，整数部分将缩小，就涉及 floor ceil 取整和变 0 问题
        expnum1.c = byFloor ? MATH_FLOOR(expnum1.c) : MATH_CEIL(expnum1.c);
    }
}
/**
 * 将两个量级数的指数对齐到较小者
 */
function expFixMin(expnum1, expnum2, byFloor) {
    if (expnum1.e < expnum2.e) {
        expFixTo(expnum2, expnum1, byFloor);
    } else {
        expFixTo(expnum1, expnum2, byFloor);
    }
}
/**
 * 基于量级计数法，对原值的整数部分取近似，不适用于负数和 0
 * @param       {Number}    num             原值
 * @param       {Array}     [rounds]        在取近似时，提供预置选项，近似到 rounds 中的某项
 * @return      {Object}    expNum          2 位精度的量级计数法对象，不小于原值
 */
function getCeil(num, rounds) {
    rounds = rounds || mySteps;
    num = expNum(num);
    // 2 位精度量级计数法
    var cNum = num.c;
    var i = 0;
    while (cNum > rounds[i]) {
        // 在预置的近似数中，找到不小于目标 cNum 的项
        i++;
    }
    if (!rounds[i]) {
        // 如果没找到合适的预置项，一定是目标值大于全部的预置项
        cNum /= 10;
        // 将目标值缩小 10 倍，重找一次定能命中
        num.e += 1;
        i = 0;
        while (cNum > rounds[i]) {
            i++;
        }
    }
    num.c = rounds[i];
    return num;
}
/**
 * 基于量级计数法的计算，必须 min < max
 */
function coreCalc(min, max, section) {
    var step;
    var secs = section || +custSecs.slice(-1);
    var expStep = getCeil((max - min) / secs, custSteps);
    // 这是可能的最小步长，以它的量级作为后续计算的基准量级，以保证整数计算特性
    var expSpan = expNum(max - min);
    // 2 位精度的最值跨度，过高的精度意味着有效数位更多
    var expMin = expNum(min, -1, 1);
    // 最小值向下近似，以涵盖原最小值
    var expMax = expNum(max, -1);
    // 最大值向上近似，参数 -1 表示保留全精度，因为要注意 min = 10000001, max = 10000002 等情况
    expFixTo(expSpan, expStep);
    // 指数对齐
    expFixTo(expMin, expStep, 1);
    // 经过指数对齐，原最大值、最小值都有可能变为 0
    expFixTo(expMax, expStep);
    if (!section) {
        secs = look4sections(expMin, expMax);
    } else {
        step = look4step(expMin, expMax, secs);
    }
    // 如果原最值都是整数，尽量让输出值也保持整数，但原最值跨 0 的则不调整
    if (MATH_ISINT(min) && MATH_ISINT(max) && min * max >= 0) {
        if (max - min < secs) {
            // 再次出现跨度小于段数
            return forInteger(min, max, secs);
        }
        secs = tryForInt(min, max, section, expMin, expMax, secs);
    }
    var arrMM = cross0(min, max, expMin.c, expMax.c);
    expMin.c = arrMM[0];
    expMax.c = arrMM[1];
    if (minLocked || maxLocked) {
        singleLocked(min, max, expMin, expMax);
    }
    return makeResult(expMin.c, expMax.c, secs, expMax.e);
}
/**
 * 在预置的可选段数中，找出一个合适的值，让跨度误差尽量小
 */
function look4sections(expMin, expMax) {
    var section;
    var tmpStep, tmpMin, tmpMax;
    var reference = [];
    for (var i = custSecs.length; i--;) {
        // 逐步减小段数，步长就会渐大
        section = custSecs[i];
        tmpStep = getCeil((expMax.c - expMin.c) / section, custSteps);
        tmpStep = tmpStep.c * MATH_POW(tmpStep.e);
        // 步长都用常规整数参与计算
        tmpMin = MATH_FLOOR(expMin.c / tmpStep) * tmpStep;
        tmpMax = MATH_CEIL(expMax.c / tmpStep) * tmpStep;
        reference[i] = {
            min: tmpMin,
            max: tmpMax,
            step: tmpStep,
            span: tmpMax - tmpMin    // 步长的误差被 段数 成倍放大，可能会给跨度造成更大的误差，使最后的段数大于预置的最大值
        };
    }
    reference.sort(function (a, b) {
        var delta = a.span - b.span;
        // 分段调整之后的跨度，一定不小于原跨度，所以越小越好
        if (delta === 0) {
            delta = a.step - b.step;    // 跨度相同时，步长小者胜出
        }
        return delta;
    });
    reference = reference[0];
    section = reference.span / reference.step;
    expMin.c = reference.min;
    expMax.c = reference.max;
    return section < 3 ? section * 2 : section;    // 如果最终步长比最小步长大得多，段数就可能变得很小
}
/**
 * 指定段数，在预置的可选步长中，找出一个合适的值，让 步长 * 段数 积刚好涵盖原最大值与最小值
 */
function look4step(expMin, expMax, secs) {
    var span;
    var tmpMax;
    var tmpMin = expMax.c;
    var tmpStep = (expMax.c - expMin.c) / secs - 1;
    while (tmpMin > expMin.c) {
        tmpStep = getCeil(tmpStep + 1, custSteps);
        tmpStep = tmpStep.c * MATH_POW(tmpStep.e);
        span = tmpStep * secs;
        tmpMax = MATH_CEIL(expMax.c / tmpStep) * tmpStep;
        tmpMin = tmpMax - span;    // 优先保证 max 端的误差最小，试看原 min 值能否被覆盖到
    }
    var deltaMin = expMin.c - tmpMin;
    // 上面的计算可能会让 min 端的误差更大，下面尝试均衡误差
    var deltaMax = tmpMax - expMax.c;
    var deltaDelta = deltaMin - deltaMax;
    if (deltaDelta > tmpStep * 1.1) {
        // 当 min 端的误差比 max 端大很多时，考虑将 tmpMin tmpMax 同时上移
        deltaDelta = MATH_ROUND(deltaDelta / tmpStep / 2) * tmpStep;
        tmpMin += deltaDelta;
        tmpMax += deltaDelta;
    }
    expMin.c = tmpMin;
    expMax.c = tmpMax;
    return tmpStep;
}
/**
 * 原最值都是整数时，尝试让输出也保持整数
 */
function tryForInt(min, max, section, expMin, expMax, secs) {
    var span = expMax.c - expMin.c;
    var step = span / secs * MATH_POW(expMax.e);
    if (!MATH_ISINT(step)) {
        // 原最值都是整数，但计算步长可能出现小数，如 2.5
        step = MATH_FLOOR(step);
        // 步长总是要尽量小，以减小跨度误差，所以 2.5 可能被调整为 2 或者 3
        span = step * secs;
        if (span < max - min) {
            step += 1;
            span = step * secs;
            if (!section && step * (secs - 1) >= max - min) {
                secs -= 1;
                span = step * secs;
            }
        }
        if (span >= max - min) {
            var delta = span - (max - min);
            // 误差均衡
            expMin.c = MATH_ROUND(min - delta / 2);
            expMax.c = MATH_ROUND(max + delta / 2);
            expMin.e = 0;
            expMax.e = 0;
        }
    }
    return secs;
}
/**
 * 整数情况下，跨度小于段数的处理
 */
function forInteger(min, max, section) {
    section = section || 5;
    if (minLocked) {
        max = min + section;    // min max 没有写错，因为 min locked 所以 max 在 min 上浮动
    } else if (maxLocked) {
        min = max - section;
    } else {
        var delta = section - (max - min);
        // 没有端点锁定时，向上下延展跨度
        var newMin = MATH_ROUND(min - delta / 2);
        var newMax = MATH_ROUND(max + delta / 2);
        var arrMM = cross0(min, max, newMin, newMax);
        // 避免跨 0
        min = arrMM[0];
        max = arrMM[1];
    }
    return makeResult(min, max, section);
}
/**
 * 非整数情况下，跨度为 0 的处理
 */
function forSpan0(min, max, section) {
    section = section || 5;
    // delta 一定不为 0 ，因为 min === max === 0 的情况会进入 forInteger 分支
    var delta = MT.min(MATH_ABS(max / section), section) / 2.1;
    if (minLocked) {
        max = min + delta;    // min max 没有写错，因为 min locked 所以 max 在 min 上浮动
    } else if (maxLocked) {
        min = max - delta;
    } else {
        // 以最值为中心，上下各延展一小段
        min = min - delta;
        max = max + delta;
    }
    return coreCalc(min, max, section);
}
/**
 * 当原始最值都在 0 的同侧时，让输出也保持在 0 的同侧
 */
function cross0(min, max, newMin, newMax) {
    if (min >= 0 && newMin < 0) {
        newMax -= newMin;
        newMin = 0;
    } else if (max <= 0 && newMax > 0) {
        newMin -= newMax;
        newMax = 0;
    }
    return [
        newMin,
        newMax
    ];
}
/**
 * 取一个数的小数位数
 * @param   {Number}    num         原数值
 * @return  {Number}    decimals    整数则返回 0 ，小数则返回小数点后的位数
 */
function decimals(num) {
    num = (+num).toFixed(15).split('.');
    // String(0.0000001) 会得到 '1e-7'
    return num.pop().replace(/0+$/, '').length;
}
/**
 * 单个最值锁定处理，只是在原计算的基础上，锁定一个，平移另一个
 */
function singleLocked(min, max, emin, emax) {
    if (minLocked) {
        var expMin = expNum(min, 4, 1);
        // 4 位精度向下近似
        if (emin.e - expMin.e > 6) {
            // 如果锁定值的量级远小于基准量级，认为锁定失败，强置为 0
            expMin = {
                c: 0,
                e: emin.e
            };
        }
        expFixMin(emin, expMin);
        // 将指数与量级较小者对齐
        expFixMin(emax, expMin);
        emax.c += expMin.c - emin.c;
        // 最大值平移
        emin.c = expMin.c;    // 最小值锁定
    } else if (maxLocked) {
        var expMax = expNum(max, 4);
        // 4 位精度向上近似
        if (emax.e - expMax.e > 6) {
            // 如果锁定值的量级远小于基准量级，认为锁定失败，强置为 0
            expMax = {
                c: 0,
                e: emax.e
            };
        }
        expFixMin(emin, expMax);
        // 将指数与量级较小者对齐
        expFixMin(emax, expMax);
        emin.c += expMax.c - emax.c;
        // 最小值平移
        emax.c = expMax.c;    // 最大值锁定
    }
}
/**
 * 最小值和最大值同时被锁定的情况在这里，其它地方只考虑单边最值锁定
 * @param   {Number}    min         锁定的最小值
 * @param   {Number}    max         锁定的最大值
 * @param   {Number}    [section]   段数
 * @return  {Object}                同 smartSteps
 */
function bothLocked(min, max, section) {
    var trySecs = section ? [section] : custSecs;
    var span = max - min;
    if (span === 0) {
        // 最大最小值都锁定到同一个值上，认为锁定失败
        max = expNum(max, 3);
        // 3 位精度向上近似
        section = trySecs[0];
        max.c = MATH_ROUND(max.c + section / 2);
        return makeResult(max.c - section, max.c, section, max.e);
    }
    if (MATH_ABS(max / span) < 0.000001) {
        // 如果锁定值远小于跨度，也认为锁定失败，强置为 0
        max = 0;
    }
    if (MATH_ABS(min / span) < 0.000001) {
        min = 0;
    }
    var step, deltaSpan, score;
    var scoreS = [
        [
            5,
            10
        ],
        [
            10,
            2
        ],
        [
            50,
            10
        ],
        [
            100,
            2
        ]
    ];
    var reference = [];
    var debugLog = [];
    var expSpan = expNum(max - min, 3);
    // 3 位精度向上近似
    var expMin = expNum(min, -1, 1);
    var expMax = expNum(max, -1);
    expFixTo(expMin, expSpan, 1);
    expFixTo(expMax, expSpan);
    span = expMax.c - expMin.c;
    expSpan.c = span;
    for (var i = trySecs.length; i--;) {
        section = trySecs[i];
        step = MATH_CEIL(span / section);
        deltaSpan = step * section - span;
        score = (deltaSpan + 3) * 3;
        // 误差越大得分越高
        score += (section - trySecs[0] + 2) * 2;
        // 分段越多得分越高
        if (section % 5 === 0) {
            // 段数为 5 可以减分
            score -= 10;
        }
        for (var j = scoreS.length; j--;) {
            // 好的步长是最重要的减分项
            if (step % scoreS[j][0] === 0) {
                score /= scoreS[j][1];
            }
        }
        debugLog[i] = [
            section,
            step,
            deltaSpan,
            score
        ].join();
        reference[i] = {
            secs: section,
            step: step,
            delta: deltaSpan,
            score: score
        };
    }
    //console.log(debugLog);
    reference.sort(function (a, b) {
        return a.score - b.score;
    });
    reference = reference[0];
    expMin.c = MATH_ROUND(expMin.c - reference.delta / 2);
    expMax.c = MATH_ROUND(expMax.c + reference.delta / 2);
    return makeResult(expMin.c, expMax.c, reference.secs, expSpan.e);
}
module.exports = smartSteps || module.exports;;